sub _svn
{
    my ($path, $info) = @_;

    package_install("subversion")
	unless in_path("svn");

    die "SVN is not available\n"
	unless in_path("svn");

    my $module = $info->{module} || die "No module for $path\n";
    my $rev =  $info->{rev} || "HEAD";

    my @svn_args = ("--non-interactive", "-r", $rev);

    if (chdir $path)
    {
	my ($url) = `svn info` =~ /^URL: (.*)/m
	    or die "Not an SVN checkout in $path\n";

	return command("svn", "update", @svn_args, {
	    success => "Updated '$module\@$rev' to $path",
	    failure => "SVN update on $path failed",
	    ignore  => qr/^At revision \d+\.$/,
	}) if $url eq $module;

	return command("svn", "switch", @svn_args, $module, {
	    success => "Updated '$module\@$rev' to $path",
	    failure => "SVN switch on $path failed",
	    ignore  => qr/^At revision \d+\.$/,
	});
    }

    return command("svn", "checkout", @svn_args, $module, $path, {
	success => "Updated '$module\@$rev' to $path",
	failure => "SVN checkout on $path failed",
    });
}

sub _cvs
{
    my ($path, $info) = @_;

    package_install("cvs")
	unless in_path("cvs");

    die "CVS is not available\n"
	unless in_path("cvs");

    my $repos = $info->{repos} || die "No repos for $path\n";
    my $module = $info->{module} || die "No module for $path\n";
    my $scrambled_pass = $info->{pass} || "A"; # empty
    my $rev = $info->{rev} || 'HEAD';
    my $prune = $info->{prune};

    my $home = (getpwuid $>)[7];
    file_append("$home/.cvspass", "/1 $repos $scrambled_pass\n", qr/^\/1 \Q$repos\E /);

    my @cvs_args = $rev eq 'HEAD' ? ('-A') : ('-r', $rev);
    push @cvs_args, "-P" if $prune;

    if (chdir $path)
    {
	die "Not a CVS checkout in $path\n"
	    unless -d "CVS";

	open REP, "<CVS/Repository" or die "no Repository file in $path/CVS: $!";
	chomp(my $check_module = <REP>);
	close REP;

	die "A different CVS module has already been checked out into $path"
	    unless $check_module eq $module or $check_module eq "CVSROOT/Emptydir";

	command("cvs", "-q", "update", "-d", @cvs_args, {
	    success => "Updated '$module:$rev' in $path",
	    failure => "CVS checkout to '$path' failed.",
	}) and die "Couldn't update: $!";
    }
    else
    {
	my ($base, $dir) = $path =~ m|^(.*?/)?([^/]+)/?$|;
	dir_check $base;
	chdir $base or die;

	command("cvs", "-q", "-d", $repos, "checkout", "-d", $dir, @cvs_args, $module, {
	    success => "Checked out '$module:$rev' to $path",
	    failure => "CVS checkout to '$path' failed.",
	}) and die "Couldn't checkout: $!";
    }
}

sub _git
{
    die;
}

my %types = (
    cvs => \&_cvs,
    svn => \&_svn,
    git => \&_git,
);

my $checkout = fetchall_hash("checkout")
    or return;

my $again = 0;
my $i = 0;

AGAIN:
foreach my $path (sort {
	($checkout->{$b}{prio} <=> $checkout->{$a}{prio}) ||
	($a cmp $b)
    } map {
	my $info = $checkout->{$_};
	$info = $checkout->{$_} = { module=>$info } unless ref $info;
	$info->{repos} ||= "";
	$info->{prio} ||= 50;
	$_
    } keys %{ $checkout })
{
    next if $Rollout::_checkout_order{ $path };
    $Rollout::_checkout_order{ $path } = ++$i;

    my $info = $checkout->{$path};

    unless ($info->{type})
    {
	local $_ = $info->{module};

	$info->{type} =
	    /^git:/ || /\.git$/ && 'git' ||
	    /^svn/ || /^https?:/ && 'svn' ||
	    $info->{repos} && 'cvs';

	die "Unknown repos type for $path\n"
	    unless $info->{type};
    }

    my $type = $info->{type};

    $types{ $type }($path, $info);

    chdir $path;

    if (-e "$path/machines.cfg")
    {
	# make a couple of local variables to be used by machines.cfg
	our ($_path, $_module, $_class) = ($path, $info->{module}, "CVS_$info->{repos}_$info->{module}");

	add_class $_class;

	do "$path/machines.cfg";
	die "$path/machines.cfg: $@" if $@;

	# the file might have modified %m to include more checkouts, so re-run
	$again++;
    }

    if ($info->{run})
    {
	$info->{run}->($info->{module}, $path)
	    if ref eq 'CODE';
	command(ref $info->{run} ? @{ $info->{run} } : $info->{run})
	    if ref ne 'CODE';
    }
}

if ($again)
{
    $checkout = fetchall_hash("checkout");
    $again = 0;
    goto AGAIN;
}

1;
